iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Mobile Development

我的 Flutter 進化論:30 天打造「Crew Up!」的架構之旅系列 第 9

Day 9 - Cache、TTL 與版本管理:實現高效能的資料快取策略

  • 分享至 

  • xImage
  •  

Day 8 建立了 LocalStorage 和 SecureStorage 的分層架構。今天,我們將一起來看看三個重要的層面:

  • Cache 策略:什麼資料要快取?如何命名?何時失效?
  • TTL 過期控制 (Time To Live Expiration Control):自動化的過期清理機制,避免伺服器端和本地資料不同步
  • 版本管理 (Versioning):當資料格式演進時,如何安全遷移而不會讓 App 出問題

在開發過程中,我們發現這些問題都需要系統化的解決方案。本文分享我們在 Crew Up!
專案中的實際落地經驗,所有程式碼範例都可以在專案中找到對應的實作。


Cache 策略與命名規範

🎯 Cache Key 命名規則

專案中實際使用的模組化命名格式:

{feature}:{dataType}[:{identifier}]

實際使用範例:

  • activity:list:活動清單(ActivityRepository)
  • activity:detail:{activityId}:活動詳情(ActivityRepository)
  • activity:categories:活動分類(ActivityRepository)
  • home:indexActivities:首頁活動清單(IndexRepository)
  • home:popularActivities:首頁熱門活動清單(PopularActivityRepository)
  • home:activeCrewers:首頁活躍 Crewer(ActiveCrewerRepository)
  • profile:user:{userId}:使用者 Profile(ProfileRepository)

💡 設計優勢:模組化命名讓 feature-first 架構中的快取管理更加清晰,前綴清理可批次清除特定模組快取,例如清除所有home: 相關快取。


TTL 設計與 LocalStorage 整合

基於 Day 8 建立的分層儲存架構,我們在 LocalStorage 層實作了 TTL 能力,將實際資料與快取中繼資料一起管理:

💡 與 Day 8 的關聯:Day 8 建立了 LocalStorage(非敏感資料)和 SecureStorage(敏感資料)的分層架構。Day 9 的 TTL 功能專門增強 LocalStorage 的快取能力,嚴格遵循安全邊界原則。

🔧 資料結構設計:

  • value:實際資料(以字串儲存,JSON 序列化)
  • cachedAt:寫入時間戳
  • ttlMs:存活時間(毫秒)

核心類別實作:

// lib/app/core/storage/cache_item.dart

// (imports omitted)

class CacheItem {
  final String value;
  final DateTime cachedAt;
  final int ttlMs;
  // ... isExpired、remainingMs、toJson/fromJson 等
}

LocalStorage 介面擴充:

// lib/app/core/storage/local_storage.dart

// (imports omitted)

/// 本地儲存抽象介面(Day 8 基礎 + Day 9 TTL 擴充)
/// 
/// 在 Day 8 建立的基礎儲存介面上,新增 TTL 快取能力
abstract class LocalStorage {
  // Day 8 的基本儲存操作
  Future<void> saveString(String key, String value);
  Future<String?> getString(String key);
  
  Future<void> saveBool(String key, bool value);
  Future<bool?> getBool(String key);
  
  Future<void> saveInt(String key, int value);
  Future<int?> getInt(String key);
  
  Future<void> saveDouble(String key, double value);
  Future<double?> getDouble(String key);
  
  Future<void> saveStringList(String key, List<String> value);
  Future<List<String>?> getStringList(String key);
  
  Future<bool> containsKey(String key);
  Future<void> remove(String key);
  Future<void> clear();
  Future<Set<String>> getKeys();

  // Day 9 新增的 TTL 相關方法
  Future<void> saveStringWithTTL(String key, String value, Duration ttl);
  Future<String?> getStringWithTTL(String key);
  Future<bool> isExpired(String key);
  Future<void> evictExpired();
  Future<CacheItem?> getCacheMetadata(String key);
}

快取管理方法整合:

LocalStorage 已整合實用的快取管理方法,無需額外的輔助類別:

// LocalStorage 介面已包含所有必要的 TTL 操作
abstract class LocalStorage {
  // 基本 TTL 操作
  Future<void> saveStringWithTTL(String key, String value, Duration ttl);
  Future<String?> getStringWithTTL(String key);
  Future<bool> isExpired(String key);
  Future<void> evictExpired();
  
  // 進階快取管理
  Future<CacheItem?> getCacheMetadata(String key);
  Future<bool> isCacheValid(String key);
  Future<int> getRemainingTime(String key);
  Future<void> evictExpiredByPrefix(String prefix);
}

🎯 Feature-Specific Cache Config

各 feature 模組的實際快取設定:

// lib/features/activity/data/activity_cache_config.dart

// (imports omitted)

/// Activity module cache configuration
class ActivityCacheConfig {
  // Cache Keys
  static const String activityListKey = 'activity:list';
  static const String activityDetailKeyPrefix = 'activity:detail:';
  static const String categoriesKey = 'activity:categories';
  
  // TTL Constants  
  static const Duration activityListTTL = Duration(minutes: 30);
  static const Duration activityDetailTTL = Duration(hours: 1);
  static const Duration categoriesTTL = Duration(hours: 4);
  
  // Helper Methods
  static String activityDetailKey(String activityId) =>
      '$activityDetailKeyPrefix$activityId';
}

Repository 層的 TTL 策略整合

在開發過程中,發現將 TTL 與 Cache 策略整合進 BaseRepository 是一個好的作法,可以提供一致且可測試的執行流程。

// lib/app/core/repositories/base_repository.dart

// (imports omitted)

/// TTL 快取策略(核心抽象)
enum CacheStrategy { 
  none,
  cacheFirst,
  remoteFirstWithCache, 
  localFirstWithCache 
}

/// 基礎 Repository 類別
/// 
/// 提供統一的錯誤處理和基礎功能,遵循 feature-first 原則
/// 不包含具體的業務邏輯,只提供基礎設施
class BaseRepository {
  final String repositoryName;
  final LocalStorage? _localStorage;

  const BaseRepository(this.repositoryName, [this._localStorage]);

  /// 執行帶有 TTL 快取的統一介面
  /// 
  /// 這是核心層提供的基礎設施,具體策略由各 feature 的 repository 實作
  Future<Result<T>> executeWithStrategy<T>(
    String operation,
    RepositoryOperation operationType,
    CacheStrategy cacheStrategy,
    String cacheKey,
    Duration ttl,
    Future<T> Function() primarySource, {
    Future<T> Function()? fallbackSource,
    String Function(T)? serializer,
    T Function(String)? deserializer,
  }) async {
    // 核心層只提供統一的框架,不包含具體實作
    // 具體的快取策略由各 feature 的組合策略實作
  }
}

📝 快取策略使用場景:

  • executeRemoteFirstWithCache:優先從遠端獲取最新資料,成功後更新快取,失敗時回退到快取(例如:活動清單、首頁資料)
  • executeStaleWhileRevalidate:立即返回快取資料(即使過期),同時在背景更新快取(例如:Profile、活躍 Crewer)
  • executeCacheFirst:快取負責資料載入,應用程式只從快取讀取,快取未命中時自動從資料源載入並更新(例如:靜態資料、設定檔)

🎯 Feature-First 架構下的組合模式設計:

遵循 feature-first 原則,採用組合模式來提供共用功能,同時保持職責分離:

// lib/app/core/repositories/cache_strategy.dart

/// 快取策略類別
///
/// 專門處理快取相關邏輯,與錯誤處理策略分離
class CacheStrategy {
  final String repositoryName;
  final LocalStorage? _localStorage;

  const CacheStrategy(this.repositoryName, this._localStorage);

  Future<Result<T>> executeRemoteFirst<T>(
    String operation,
    String cacheKey,
    Duration ttl,
    Future<T> Function() remoteSource, {
    Future<T> Function()? fallbackSource,
    String Function(T)? serializer,
    T Function(String)? deserializer,
  }) async { /* 實作細節 */ }

  Future<Result<T>> executeCacheFirst<T>(
    String operation,
    String cacheKey,
    Duration ttl,
    Future<T> Function() primarySource, {
    Future<T> Function()? fallbackSource,
    String Function(T)? serializer,
    T Function(String)? deserializer,
  }) async { /* 實作細節 */ }

  /// 執行 Stale-While-Revalidate 策略
  Future<Result<T>> executeStaleWhileRevalidate<T>(
    String operation,
    String cacheKey,
    Duration ttl,
    Future<T> Function() primarySource, {
    Future<T> Function()? fallbackSource,
    String Function(T)? serializer,
    T Function(String)? deserializer,
  }) async { /* 實作細節 */ }
}
// lib/app/core/repositories/enhanced_base_repository.dart

/// 增強版基礎 Repository 類別

class EnhancedBaseRepository extends BaseRepository {
  late final cache.CacheStrategy _cacheStrategy;

  EnhancedBaseRepository(String repositoryName, [LocalStorage? localStorage])
    : super(repositoryName, localStorage) {
    _cacheStrategy = cache.CacheStrategy(repositoryName, localStorage);
  }

  /// 執行遠端優先快取策略
  Future<Result<T>> executeRemoteFirstWithCache<T>(
    String operation,
    String cacheKey,
    Duration ttl,
    Future<T> Function() remoteSource, {
    Future<T> Function()? fallbackSource,
    String Function(T)? serializer,
    T Function(String)? deserializer,
  }) => _cacheStrategy.executeRemoteFirst(
    operation, cacheKey, ttl, remoteSource,
    fallbackSource: fallbackSource,
    serializer: serializer,
    deserializer: deserializer,
  );
  
  Future<Result<T>> executeCacheFirst<T>(
    String operation,
    String cacheKey,
    Duration ttl,
    Future<T> Function() primarySource, {
    Future<T> Function()? fallbackSource,
    String Function(T)? serializer,
    T Function(String)? deserializer,
  }) => _cacheStrategy.executeCacheFirst(
    operation, cacheKey, ttl, primarySource,
    fallbackSource: fallbackSource,
    serializer: serializer,
    deserializer: deserializer,
  );

  /// 執行 Stale-While-Revalidate 策略
  Future<Result<T>> executeStaleWhileRevalidate<T>(
    String operation,
    String cacheKey,
    Duration ttl,
    Future<T> Function() primarySource, {
    Future<T> Function()? fallbackSource,
    String Function(T)? serializer,
    T Function(String)? deserializer,
  }) => _cacheStrategy.executeStaleWhileRevalidate(
    operation, cacheKey, ttl, primarySource,
    fallbackSource: fallbackSource,
    serializer: serializer,
    deserializer: deserializer,
  );
}

✅ Feature-First 架構原則:

  • 職責分離CacheStrategy 處理快取邏輯
  • 增強繼承:透過 EnhancedBaseRepository 整合快取策略類別
  • 依賴明確:通過建構函數明確注入依賴,避免隱式依賴
  • 易於測試:可以獨立測試和模擬快取策略組件
  • 符合 SOLID:每個類別專注單一職責,遵循開放封閉原則

實作案例分享

以下展示核心的 TTL 快取實作,每個 feature 使用自己的 cache config 並透過增強繼承模式享受統一的快取能力:

🎯 Activity Repository - executeRemoteFirstWithCache 策略

// lib/features/activity/data/repositories/activity_repository_impl.dart

class ActivityRepositoryImpl extends EnhancedBaseRepository
    implements ActivityRepository {
  @override
  Future<Result<List<Activity>>> getActivities() async =>
      executeRemoteFirstWithCache( // 先打遠端,失敗時用快取
        'Fetching ${ActivityCacheConfig.activityListKey}',
        ActivityCacheConfig.activityListKey,
        ActivityCacheConfig.activityListTTL, // 30分鐘 TTL
        () async {
          final remoteActivities = await _remoteDataSource.getAllActivities();
          // 成功後同步到本地
          await executeSafeSync('Syncing activities to local', () async {
            for (final activity in remoteActivities) {
              await _localDataSource.saveActivity(activity);
            }
          });
          return remoteActivities;
        },
        fallbackSource: () async => await _localDataSource.getAllActivities(),
        serializer: (activities) =>
            jsonEncode(activities.map((a) => a.toJson()).toList()),
        deserializer: (jsonString) {
          final List<dynamic> jsonList = jsonDecode(jsonString);
          return jsonList.map((json) => Activity.fromJson(json)).toList();
        },
      );
}

🏠 Index Repository - executeRemoteFirstWithCache 策略

// lib/features/home/data/repositories/index_repository_impl.dart

class IndexRepositoryImpl extends EnhancedBaseRepository
    implements IndexRepository {
  @override
  Future<Result<List<Activity>>> getIndexActivities() async =>
      executeRemoteFirstWithCache<List<Activity>>( // 先打遠端,失敗時用快取
        'Fetching ${HomeCacheConfig.indexActivitiesKey}',
        HomeCacheConfig.indexActivitiesKey,
        HomeCacheConfig.indexDataTTL, // 20分鐘 TTL
        () async => await _remoteDataSource.getIndexActivities(),
        fallbackSource: () async => await _localDataSource.getIndexActivities(),
        serializer: (activities) =>
            jsonEncode(activities.map((a) => a.toJson()).toList()),
        deserializer: (jsonString) {
          final List<dynamic> jsonList = jsonDecode(jsonString);
          return jsonList.map((json) => Activity.fromJson(json)).toList();
        },
      );
}

👥 ActiveCrewer Repository - executeStaleWhileRevalidate 策略

// lib/features/home/data/repositories/active_crewer_repository_impl.dart

class ActiveCrewerRepositoryImpl extends EnhancedBaseRepository
    implements ActiveCrewerRepository {
  @override
  Future<Result<List<ActiveCrewer>>> getActiveCrewers() async =>
      executeStaleWhileRevalidate<List<ActiveCrewer>>( // 立即返回快取,背景更新
        'Fetching ${HomeCacheConfig.activeCrewersKey}',
        HomeCacheConfig.activeCrewersKey,
        HomeCacheConfig.activeCrewersTTL, // 15分鐘 TTL
        () async => await _remoteDataSource.getActiveCrewers(),
        fallbackSource: () async => await _localDataSource.getActiveCrewers(),
        serializer: (activeCrewers) =>
            jsonEncode(activeCrewers.map((c) => c.toJson()).toList()),
        deserializer: (jsonString) {
          final List<dynamic> jsonList = jsonDecode(jsonString);
          return jsonList.map((json) => ActiveCrewer.fromJson(json)).toList();
        },
      );
}

📋 不同策略的使用場景:

  • executeRemoteFirstWithCache:Activity 和 Index 都需要較新的資料,先嘗試遠端獲取最新內容,失敗時使用快取
  • executeStaleWhileRevalidate:ActiveCrewer 可容忍稍微過期的資料,立即返回快取內容提升用戶體驗,同時背景更新
  • executeCacheFirst:適用於變動較少的靜態資料,快取層負責資料載入

📊 Feature-First 架構的 TTL 快取總結:

  1. 模組獨立性:每個 feature 擁有自己的 cache config,TTL 設定,和快取鍵值
  2. 職責分離:核心層(app/core/)提供基礎設施,feature 層實作具體業務邏輯
  3. 增強繼承優勢:採用增強繼承模式,提供更清晰的依賴注入和職責分離
  4. 可維護性:修改某個 feature 的快取策略不會影響其他 feature
  5. 團隊協作:不同團隊成員可以獨立開發不同 feature 的快取邏輯
  6. 測試友善:每個策略類別可以獨立測試,避免複雜的依賴關係
  7. 符合 SOLID:每個類別專注單一職責,遵循開放封閉原則

版本升級規則(專案規範)

  1. 只在「資料格式或語意變更」時提升版本號(例如:JSON 結構改動、欄位改名/型別變更)。
  2. 每次僅升級 +1(vN → vN+1),避免跨級複雜遷移。
  3. 遷移中嚴禁讀取網路;僅對「本地資料」進行清理或格式轉換。
  4. 無法判斷或未知版本時,採取「保守清除」策略,確保 App 能啟動且不讀到髒資料。

遷移腳本範例(v1 → v2)

目標:將舊的 home: 快取清空(含對應 metadata),並保留其他資料。

// lib/app/core/storage/local_storage.dart

Future<void> _migrateData(int from, int to) async {
  developer.log('📦 執行資料遷移: v$from -> v$to', name: 'LocalStorageImpl');

  if (from == 1 && to >= 2) {
    await _removeByPrefix('home:');
    return;
  }

  if (from == 0 && to >= 1) {
    return;
  }

  developer.log('⚠️ 未知的儲存版本,清空快取', name: 'LocalStorageImpl');
  await _preferences!.clear();
  await _preferences!.setInt(_storageVersionKey, _currentStorageVersion);
}

Future<void> _removeByPrefix(String prefix) async {
  final keys = _preferences!.getKeys();
  for (final key in keys) {
    if (key.startsWith(prefix)) {
      await _preferences!.remove(key);
      final metadataKey = '${key}_metadata';
      if (keys.contains(metadataKey)) {
        await _preferences!.remove(metadataKey);
      }
    }
  }
}

✅ 原則:從一開始就要建立版本,既使還沒有要做遷移,也要未雨綢繆。


與前幾天的關聯

  • Day 1 Clean Architecture:TTL 快取策略完美實現依賴反轉原則,核心層定義抽象,各 feature 實作具體策略
  • Day 6 Riverpod 狀態管理:透過 Riverpod 的依賴注入,TTL 策略能在不同功能模組無痛套用,與狀態管理形成統一資料流
  • Day 7 Repository 模式:TTL 策略以 Repository 為單位落地,與 Result、Fallback 策略完整整合,增強資料存取能力
  • Day 8 儲存層:在 Day 8 建立的 LocalStorage(非敏感)與 SecureStorage(敏感)分層架構基礎上,TTL
    功能專門增強 LocalStorage 的快取能力,嚴格遵循安全邊界原則

設計原則總結

今天的 TTL 快取實作體現了幾個重要的設計原則:

🛡️ 防禦思維

  • 永遠有 fallback:每個快取策略都配備後備方案
  • 保守遷移:版本升級時採用安全優先的策略

⚡ 業務導向

  • 差異化 TTL:根據資料特性設定不同的過期時間
  • 策略選擇:executeRemoteFirstWithCache vs Stale-While-Revalidate (executeStaleWhileRevalidate) 依場景決定

🔧 組合模式優勢

  • 職責分離:CacheStrategy 專注快取邏輯處理
  • 依賴明確:通過建構函數注入,避免隱式依賴
  • 易於測試:獨立的策略類別可單獨驗證


下一步

明天,我們將深入探討「錯誤處理與日誌記錄」,學習如何建立完整的錯誤追蹤機制、統一的異常處理策略,以及可觀測的系統日誌,讓開發和維護過程更加順暢。

期待與您在 Day 10 相見!


📋 相關資源

📝 專案資訊

  • 專案名稱: Crew Up!
  • 開發日誌: Day 9 - Cache、TTL 與版本管理:實現高效能的資料快取策略
  • 文章日期: 2025-09-22
  • 技術棧: Flutter, Clean Architecture, SharedPreferences, TTL, Riverpod

上一篇
Day 8 - 儲存架構設計:分層儲存策略與安全邊界
下一篇
Day 10 - 錯誤處理與日誌記錄:建立錯誤追蹤機制
系列文
我的 Flutter 進化論:30 天打造「Crew Up!」的架構之旅10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言